#include <buola/gui/cwindowmanager.h>

#include <buola/gui/cwindow.h>
#include <buola/gui/cphysicalwindow.h>
#include <buola/gui/ctoolkit.h>
#include <buola/gui/caccelerator.h>

namespace buola { namespace gui {

CWindowManager::CWindowManager()
    :   mPointerWindow(nullptr)
    ,   mFocusWindow(nullptr)
    ,   mGrabFocusWindow(nullptr)
    ,   mDragWindow(nullptr)
{
    get_main_event_loop().AddFunction(CEventLoop::PRIORITY_LOWEST,this);
}
    
bool CWindowManager::OnLoopFunction()
{
    ///\todo we must do the real thing here!
    using std::swap;
    
    bool lReturn=true;
    
    if(!mLayout.empty())
    {
        lReturn=false;
        std::set<CWindow*> lLayout;
        swap(lLayout,mLayout);
        
        for(CWindow *w : lLayout)
            w->DoLayout();
    }
    
    if(!mRefresh.empty())
    {
        lReturn=false;
        std::set<CWindow*> lRefresh;
        swap(lRefresh,mRefresh);
        
        for(CWindow *f : lRefresh)
            f->DoRefresh(CRegion());
    }
    
    get_toolkit()->Synchronize();
    
    return lReturn;
}

///\todo maybe we should allow freezing ordinary windows. for now, frames will do.
void CWindowManager::FreezeModal(CWindow *pWindow,bool pRepeat)
{
    static CWindow *lLastWindow=nullptr;

    if(pRepeat)
        pWindow=lLastWindow;
    else
        lLastWindow=pWindow;

    for(CWindow *w : mAll)
    {
        if(pWindow&&!w->IsDescendantOf(*pWindow))
            w->Freeze();
        else
            w->Defreeze();
    }
}

void CWindowManager::OnPhysicalKey(CKeyEvent *pE)
{
    bool lDispatchNormally=CAccelerator::Dispatch(*pE); //try accelerator first
    if(!lDispatchNormally) return;

    pE->mTarget=GrabFocusWindow();           //grabbed focus
    if(!pE->mTarget) pE->mTarget=FocusWindow();                //normal focus
    if(!pE->mTarget)
    {
        msg_debug<5>() << "Key event not dispatched to any window\n";
        return;
    }

    ///\todo should dispatch key event... it is already sent to the right window, so we don't need to send it to a frame
    ///\todo maybe this should actually go to the window itself, since it has already been assigned. or maybe the frame should take
///care of delivering to the right window
    ///\todo reimplement freezing
//     if(lTarget->Frozen()) 
//     {
//         msg_warn() << "physical key received while frozen... shouldn't happen";
//         return;
//     }
    
    PDeleteTracker lGuard(pE->mTarget->GetDeleteTracker());
    
    if(pE->mKey.GetCode()==KEY_TAB&&pE->mType==CKeyEvent::PRESS)
    {
        if(pE->mTarget->FocusMode()&CWindow::EFocusMode::TAB)
        {
            if(pE->mMods==KEYMOD_NONE)
                pE->mTarget->Parent()->CirculateFocus(CWindow::ECirculate::NEXT,pE->mTarget,true);
            else if(pE->mMods==KEYMOD_SHIFT)
                pE->mTarget->Parent()->CirculateFocus(CWindow::ECirculate::PREVIOUS,pE->mTarget,true);
        }
    }
    
    if(lGuard->Deleted())
        return;
    
    if(pE->mType==CKeyEvent::PRESS)
    {
        if(pE->mTarget->Events()&EVENT_CHAR)
        {
            if(pE->mPlainChar)
                pE->mTarget->OnChar(*pE);

            if(lGuard->Deleted())
                return;
        }
        
        if(pE->mTarget->Events()&EVENT_KEYDOWN)
            pE->mTarget->OnKeyDown(*pE);
    }
    else if(pE->mType==CKeyEvent::RELEASE)
    {
        if(pE->mTarget->Events()&EVENT_KEYUP)
            pE->mTarget->OnKeyUp(*pE);
    }
}

void CWindowManager::OnPhysicalMouse(CMouseEvent *pE,CPhysicalWindow *pWindow)
{
    pE->mTarget=pWindow->GetWindow().get();

    if(mDragWindow)
    {
        pE->mTarget=mDragWindow;
        if(pE->mButtons==EMouseButton::NONE)
            mDragWindow=nullptr;
    }
    else
    {
        ///\todo we are now using MOUSETRANSPARENT as the way to pass events to parent... maybe it would be better to have the
        ///handlers return a boolean which says if they handled the event
        ///\todo right now we can only freeze frames. should it change?
        //if(lTarget->Frozen()) return;

        pE->mTarget=pE->mTarget->FindMouseTargetAt(pE->mPoint);

        while(pE->mTarget->InputMode()&CWindow::EInputMode::MOUSETRANSPARENT)
        {
            if(pE->mTarget->Parent())
                pE->mTarget=pE->mTarget->mParent;
            else
                return;
        }
        
        if(pE->mButtons!=EMouseButton::NONE)
            mDragWindow=pE->mTarget;
    }
    
    if(!pE->mTarget) return;

    pE->mPoint-=pE->mTarget->GetPosInTopLevel();
    pE->mPoint=pE->mTarget->DeviceToClient(pE->mPoint);

    if(pE->mType==CMouseEvent::MOVEIN||pE->mType==CMouseEvent::MOTION)
    {
        if(pE->mTarget!=mPointerWindow)
        {
            if(mPointerWindow)
            {
                pE->mType=CMouseEvent::MOVEOUT;
                mPointerWindow->OnMouseOut(*pE);
            }
            mPointerWindow=pE->mTarget;
            pE->mType=CMouseEvent::MOVEIN;
            mPointerWindow->OnMouseIn(*pE);
            pWindow->SetPointer(mPointerWindow->mPointer.get());
        }
        pE->mType=CMouseEvent::MOTION;
        pE->mTarget->OnMouseMove(*pE);
    }
    else if(pE->mType==CMouseEvent::MOVEOUT)
    {
        if(mPointerWindow&&mPointerWindow!=mDragWindow)
        {
            mPointerWindow->OnMouseOut(*pE);
            mPointerWindow=nullptr;
        }
    }
    else if(pE->mType==CMouseEvent::PRESS)
    {
        if(pE->mTarget->FocusMode()&CWindow::EFocusMode::MOUSE)
            pE->mTarget->SetFocus();

        /*
        for(int i=0;i<lTarget->mLayoutManagers.size();i++)
        {
            lTarget->mLayoutManagers[i]->OnButtonDown(pE);
        }
        */

        pE->mTarget->OnButtonDown(*pE);
    }
    else if(pE->mType==CMouseEvent::RELEASE)
    {
        pE->mTarget->OnButtonUp(*pE);
        ///\todo there was something about button repeaters here
    }
    else if(pE->mType==CMouseEvent::WHEEL)
    {
        pE->mTarget->OnMouseWheel(*pE);
    }
}

void CWindowManager::OnPhysicalExpose(CPhysicalWindow *pWindow,const CRect_d &pRect,bool /*pMore*/,bool /*pSendEvent*/)
{
    msg_info() << "OnPhysicalExpose\n";
    ///\todo see what to do with the immediate thing, which is only for cocoa
    ///\todo this should trigger the actual refresh, which should actually be scheduled (except in cocoa)
    ///\todo even in cocoa, make sure this never happens before the layout recalculation caused by a previous PhysicalSize
    ///event that is currently scheduled
    CWindow *lTarget=pWindow->GetWindow().get();
    
    if(!lTarget->Valid(CWindow::EValid::INTERNAL_LAYOUT|CWindow::EValid::EXTERNAL_LAYOUT))
    {
        mLayout.erase(lTarget);
        lTarget->DoLayout();
    }
    
    mRefresh.erase(lTarget);
    lTarget->DoRefresh(nUnbounded);
}

void CWindowManager::OnPhysicalGeometry(CPhysicalWindow *pWindow,const CPoint_d &pPos,const CSize_d &pSize)
{
    CWindow *lTarget=pWindow->GetWindow().get();
    if(lTarget->Mapped())
        lTarget->ProcessMap(CRect_d(pPos,pSize));
}

void CWindowManager::OnPhysicalMap(CPhysicalWindow *pWindow,bool pMapped)
{
    CWindow *lTarget=pWindow->GetWindow().get();
    
    if(pMapped)
    {
        if(lTarget->Mapped()) return;
        msg_info() << "map granted\n";
    
        //this is called from here. the function that actually causes the mapping just does nothing and waits until the window
        //manager informs us that it's happened
        lTarget->ProcessMap(CRect_d(pWindow->GetPos(),pWindow->GetSize()));
    }
    else
    {
        ///\todo this!!!
        msg_info() << "unmap granted\n";
    }
}

void CWindowManager::OnPhysicalCloseWindow(CPhysicalWindow *pWindow)
{
    CWindow *lTarget=pWindow->GetWindow().get();
    
    lTarget->Close();
}

void CWindowManager::OnPhysicalActivate(CPhysicalWindow *pWindow,bool pActive)
{
    CWindow *lTarget=pWindow->GetWindow().get();
    
    if(pActive)
    {
        if(lTarget==mActiveWindow) return;
        mActiveWindow=lTarget;

        SetFocusTo(lTarget->FindFocusedDescendant());
    }
    else
    {
        if(lTarget!=mActiveWindow) return;
        mActiveWindow=nullptr;

        if(mFocusWindow->IsDescendantOf(*lTarget))
            SetFocusTo(nullptr);
    }
}

void CWindowManager::OnPointerChanged(CWindow *pWindow)
{
    if(pWindow==mPointerWindow)
    {
        CPhysicalWindow *lPhysical=pWindow->GetTopLevel()->Physical();
        if(lPhysical)
            lPhysical->SetPointer(pWindow->Pointer());
    }
}

/////////////////
// event loops
//

void CWindowManager::ModalLoop(CWindow *pFrame)
{
    bool lExit=false;

    CWindow *lOldModal=mModalFrame;
    mModalFrame=pFrame;

    window_manager().FreezeModal(mModalFrame);

    mModalLoopExit=false;
    while((!get_main_event_loop().GetExit())&&(!lExit))
    {
        get_main_event_loop().LoopUntil(mModalLoopExit);

        for(int i=0;i<mExitModal.size();i++)
        {
            if(mExitModal[i]==pFrame)
            {
                lExit=true;
                mExitModal.erase(mExitModal.begin()+i);
                ///\todo allow similar behavior
                //if(pFrame->GetAutoDelete())
                //    delete pFrame;
            }
        }

        mModalLoopExit=false;
    }

    mModalFrame=lOldModal;
    window_manager().FreezeModal(mModalFrame);
}

void CWindowManager::ExitModal(CWindow *pWindow)
{
    mModalLoopExit=true;
    mExitModal.push_back(pWindow);
}

void CWindowManager::SetFocusTo(CWindow *pWindow)
{
    if(mFocusWindow==pWindow) return;
    
    if(mFocusWindow)
        mFocusWindow->OnFocusOut(CFocusEvent());
    mFocusWindow=pWindow;
    if(mFocusWindow)
        mFocusWindow->OnFocusIn(CFocusEvent());
}

void CWindowManager::SetGrabFocusTo(CWindow *pWindow)
{
    mGrabFocusWindow=pWindow;
}

CWindowManager &window_manager()
{
    static CWindowManager lManager;
    
    return lManager;
}

static std::vector<CWindow*> sMainWindows;

///\todo jkljkljkl

void set_main_window(const PWindow &pWindow)
{
//    pWindow->SetFlag(CWindow::FLAG_MAINWINDOW);
}

PWindow get_main_window()
{
//    for(int i=0;i<CWindow::mAllWindows.size();i++)
//    {
//        if(CWindow::mAllWindows[i]->GetFlag(CWindow::FLAG_MAINWINDOW))
//            return CWindow::mAllWindows[i];
//    }
    
    return nullptr;
}

void get_main_windows(std::vector<PWindow> &pWindows)
{
//    pWindows.clear();
//    
//    for(int i=0;i<CWindow::mAllWindows.size();i++)
//    {
//        if(CWindow::mAllWindows[i]->GetFlag(CWindow::FLAG_MAINWINDOW))
//            pWindows.push_back(CWindow::mAllWindows[i]);
//    }
}

bool close_all_main_windows(bool pForce)
{
//    bool lOneFound=false;
    
//    for(int i=0;i<CWindow::mAllWindows.size();i++)
//    {
//        if(CWindow::mAllWindows[i]->GetFlag(CWindow::FLAG_MAINWINDOW))
//        {
//            if(!CWindow::mAllWindows[i]->Close(pForce))
//                return false;
//            lOneFound=true;
//        }
//    }

//    if(!lOneFound) buola_exit();

    return true;
}

/*namespace gui*/ } /*namespace buola*/ }
